﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security.Principal;
using FinWeb3.Models.Helpers;
using FinWeb3.Models.ModelView;
using FinWeb3.Models.Repository;

namespace FinWeb3.Models
{
    public class BillManager
    {
        private IRepository<Bill> repository;
        private IRepository<Group> groupRepository;
        private IRepository<Budget> budgetRepository;
        private IIdentity user;


        public BillManager(IRepository<Bill> repository, IRepository<Group> groupRepository, IIdentity user)
        {
            this.repository = repository;
            this.groupRepository = groupRepository;
            this.budgetRepository = new BudgetRepository();
            this.user = user;
        }

        public List<Bill> Get(DateTime fromDate, DateTime toDate, BillList billList)
        {
            return ProcessBills(fromDate, toDate, billList).ToList();
        }

        public List<Bill> Get(DateTime fromDate, DateTime toDate, BillList billList, bool includeChilds)
        {
            return ProcessBills(fromDate, toDate, billList, includeChilds).ToList();
        }

        public List<Bill> Get(DateTime toDate, BillList billList)
        {
            return ProcessBills(new DateTime(1900, 01, 01), toDate, billList).ToList();
        }

        public List<Bill> Get(int groupId, DateTime fromDate, DateTime toDate, BillList billList)
        {
            return ProcessBills(fromDate, toDate, billList, groupId, false).ToList();
        }

        public List<Bill> Get(int groupId, BillList billList)
        {
            List<Bill> bills = null;

            switch (billList)
            {
                case BillList.All:
                    bills = repository.GetAll(user).Where(x => x.GroupId == groupId).ToList();
                    break;
                case BillList.NotPayed:
                    bills = repository.GetAll(user).Where(x => x.IsPaid == false && x.GroupId == groupId).ToList();
                    break;
                case BillList.NotAssociated:
                    bills = repository.GetAll(user).Where(x => !x.StatementId.HasValue && x.GroupId == groupId).ToList();
                    break;
                default:
                    break;
            }

            return bills.ToList();
        }

        public List<TotalByCategory> GetTotals(DateTime fromDate, DateTime toDate)
        {
            var result = from d in Bill.All()
                         where
                            d.UserName == user.Name &&
                            d.DeletedBill == false &&
                            d.HasDetailList == false &&
                            d.Day >= fromDate &&
                            d.Day <= toDate &&
                            d.Wallet != (int)WalletBillStatus.InWallet
                         group d by new { d.Day.Year, d.Day.Month, d.Category } into g
                         select new
                         {
                             Month = g.Key.Month,
                             Year = g.Key.Year,
                             Category = g.Key.Category,
                             Total = g.Sum(x => x.BaseValue)
                         };

            var groups = from r in result.ToList()
                         group r by new { r.Category } into g
                         where g.Sum(x => x.Total) != 0
                         select new TotalByCategory
                         {
                             Month = 0,
                             Year = 0,
                             Category = g.Key.Category,
                             Total = g.Sum(x => x.Total)
                         };

            return groups.ToList();
        }

        public Bill Get(int id)
        {
            var bill = repository.Get(id, user);

            if (bill.HasDetailList)
            {
                bill.DetailLines = GetDetailLine(bill.Id);
            }

            return bill;
        }

        public void Delete(int id)
        {
            var bill = Get(id);

            if (bill != null)
            {
                bill.DeletedBill = true;
                repository.Save(bill, user);
            }
        }

        public void UndoDelete(int id)
        {
            var bill = Get(id);

            if (bill != null)
            {
                bill.DeletedBill = false;
                repository.Save(bill, user);
            }
        }

        public void Pay(int id)
        {
            var bill = Get(id);

            if (bill != null)
            {
                bill.IsPaid = true;
                repository.Save(bill, user);
            }
        }

        public void Pay(int billIdToBePaid, int billIdToPayWith)
        {
            var billToBePaid = Get(billIdToBePaid);
            var billToPayWith = Get(billIdToPayWith);

            if (billToBePaid != null && billToPayWith != null)
            {
                billToBePaid.ParentBillId = billToPayWith.Id;
                Save(billToBePaid);
            }
        }

        public void UndoPay(int id)
        {
            var bill = Get(id);

            if (bill != null)
            {
                bill.IsPaid = false;
                repository.Save(bill, user);
            }
        }

        public void Save(Bill bill)
        {
            if (bill.Wallet == (int)WalletBillStatus.InWallet)
            {
                bill.Category = "Carteira";
            }
            else if (bill.Wallet == (int)WalletBillStatus.OutWallet)
            {
                bill.IsPaid = true;
            }
            if (bill.HasDetailList)
            {
                bill.Category = string.Empty;
                bill.BaseValue = 0M;
            }
            repository.Save(bill, user);
        }

        public void Save(BillModelView bill)
        {
            Bill newBill = new Bill()
            {
                Category = bill.Category,
                Day = bill.Day,
                Description = bill.Description,
                HasDetailList = bill.HasDetailList,
                ReceiveBill = bill.ReceiveBill,
                Comments = bill.Comments,
                UserName = user.Name,
                Value = (bill.HasDetailList) ? 0M : -bill.BaseValue,
                Wallet = bill.Wallet
            };

            if (bill.IsRecurrent)
            {
                var groupManager = new GroupManager(groupRepository, repository, user);

                Group grp = new Group()
                {
                    Category = bill.Category,
                    Day = bill.Day.Day,
                    Description = bill.Description,
                    HasDetailList = bill.HasDetailList,
                    ReceiveBill = bill.ReceiveBill,
                    UserName = user.Name,
                    Value = (bill.HasDetailList) ? 0M : -bill.BaseValue
                };
                grp.FirstDay = (bill.FirstDay.HasValue) ? bill.FirstDay.Value : bill.Day.FirstDay();
                grp.LastDay = (bill.LastDay.HasValue) ? bill.LastDay.Value : grp.FirstDay;

                groupManager.Save(grp);

                newBill.GroupId = grp.Id;
            }

            Save(newBill);
        }

        public void AssociateBillToStatement(int statementId, int billId)
        {
            var statement = Statement.Find(x => x.Id == statementId && x.UserName == user.Name).FirstOrDefault();
            var bill = Get(billId);

            if (statement != null && bill != null)
            {
                bill.StatementId = statement.Id;
                bill.IsPaid = true;
                Save(bill);
            }
        }

        public List<Bill> GetDetailLine(int id)
        {
            return repository.GetAll(user).Where(x => x.ParentBillId == id).OrderBy(x => x.Day).ToList();
        }

        public void SaveDetailLine(BillDetailModelView detail)
        {
            if (detail.NewDetail.Id > 0)
            {
                var detailLine = detail.NewDetail;
                detailLine.ParentBillId = detail.BillId;

                repository.Save(detailLine, user);
            }
            else
            {
                if (detail.Instalment <= 1)
                {
                    var detailLine = detail.NewDetail;
                    detailLine.ParentBillId = detail.BillId;
                    detailLine.BaseValue = -detailLine.BaseValue;

                    repository.Save(detailLine, user);
                }
                else
                {
                    var bill = Get(detail.BillId);
                    var bills = Get(bill.GroupId.Value, bill.Day, bill.Day.AddMonths(detail.Instalment - 1), BillList.All);
                    for (int i = 0; i < bills.Count; i++)
                    {
                        var detailLine = new Bill();
                        detailLine.ParentBillId = bills[i].Id;
                        detailLine.Category = detail.NewDetail.Category;
                        detailLine.Day = detail.NewDetail.Day.AddMonths(i);
                        detailLine.Description = detail.NewDetail.Description + string.Format(" ({0}/{1})", (i + 1), detail.Instalment);
                        detailLine.Value = -detail.NewDetail.Value / detail.Instalment;
                        detailLine.UserName = user.Name;

                        Save(detailLine);
                    }
                }
            }
        }

        public void DetachDetailLine(int id)
        {
            var bill = Get(id);

            if (bill != null)
            {
                bill.ParentBillId = null;
                repository.Save(bill, user);
            }
        }

        protected virtual IEnumerable<Bill> ProcessBills(DateTime fromDate, DateTime toDate, BillList billList)
        {
            return ProcessBills(fromDate, toDate, billList, null, false);
        }

        protected virtual IEnumerable<Bill> ProcessBills(DateTime fromDate, DateTime toDate, BillList billList, bool includeChildBills)
        {
            return ProcessBills(fromDate, toDate, billList, null, includeChildBills);
        }

        protected virtual IEnumerable<Bill> ProcessBills(DateTime fromDate, DateTime toDate, BillList billList, int? groupId, bool includeChildBills)
        {
            List<Bill> bills = null;
            var groupManager = new GroupManager(groupRepository, repository, user);

            List<Group> groups = null;

            if (groupId.HasValue)
            {
                bills = repository.GetAll(user).Where(x => x.Day >= fromDate.FirstDay() && x.Day <= toDate.LastDay() && x.GroupId == groupId).ToList();
                groups = groupManager.Get(fromDate, toDate).Where(x => x.Id == groupId).ToList();
            }
            else
            {
                bills = repository.GetAll(user).Where(x => x.Day >= fromDate.FirstDay() && x.Day <= toDate.LastDay()).ToList();
                groups = groupManager.Get(fromDate, toDate);
            }

            foreach (var item in groups)
            {
                for (DateTime date = fromDate.FirstDay(); date <= toDate.LastDay(); date = date.AddMonths(1))
                {
                    // ignore groups with first date (month and year) greater then current date being processed
                    if (item.FirstDay.FirstDay() > date || (item.LastDay != item.FirstDay && item.LastDay < date))
                        continue;

                    if (bills.Exists(x => x.GroupId == item.Id && x.Day.Year == date.Year && x.Day.Month == date.Month))
                        continue;

                    var newBill = new Bill
                    {
                        Category = item.Category,
                        Day = new DateTime(date.Year, date.Month, item.Day),
                        Description = item.Description,
                        GroupId = item.Id,
                        Value = item.Value,
                        UserName = user.Name,
                        HasDetailList = item.HasDetailList,
                        ReceiveBill = item.ReceiveBill
                    };
                    newBill.Save();

                    bills.Add(newBill);
                }
            }

            // add budgets
            var budgets = GetBudgets(fromDate, toDate);

            foreach (var budget in budgets)
            {
                var newBill = new Bill
                {
                    Category = budget.Category,
                    Day = budget.ShowAt,
                    Description = "Budget - " + budget.Category,
                    Value = budget.RemainValue,
                    UserName = user.Name,
                    BankId = budget.BankId
                };

                bills.Add(newBill);
            }

            // return just bills inside asked bounds
            IEnumerable<Bill> processedBills = null;

            if (includeChildBills)
            {
                processedBills = bills
                .Where(x => !x.DeletedBill && x.Day >= fromDate && x.Day <= toDate);
            }
            else
            {
                processedBills = bills
                .Where(x => !x.DeletedBill && x.Day >= fromDate && x.Day <= toDate && x.ParentBillId == null);
            }

            List<Bill> filteredBills = null;

            switch (billList)
            {
                case BillList.All:
                    filteredBills = processedBills.ToList();
                    break;
                case BillList.NotPayed:
                    filteredBills = processedBills.Where(x => !x.IsPaid && !x.StatementId.HasValue && x.Wallet != (int)WalletBillStatus.OutWallet).ToList();
                    break;
                case BillList.NotAssociated:
                    filteredBills = processedBills.Where(x => !x.StatementId.HasValue && x.Wallet != (int)WalletBillStatus.OutWallet).ToList();
                    break;
                default:
                    filteredBills = processedBills.ToList();
                    break;
            }

            foreach (var bill in filteredBills)
            {
                if (bill.HasDetailList)
                {
                    bill.DetailLines = GetDetailLine(bill.Id);
                    bill.Category = string.Empty;
                    bill.BaseValue = 0M;
                }
            }

            return filteredBills
                .OrderByDescending(x => x.BaseValue)
                .OrderBy(x => x.Day);
        }

        private List<Budget> GetBudgets(DateTime fromDate, DateTime toDate)
        {
            var registeredBudgets = budgetRepository.GetAll(user).Where(x => x.ShowAt >= fromDate && x.ShowAt <= toDate).ToList();

            for (int i = 0; i < registeredBudgets.Count ; i++)
            {
                registeredBudgets[i].SpentValue = 
                    repository.GetAll(user)
                    .Where(
                        bill => bill.Day >= registeredBudgets[i].FromDate && 
                        bill.Day <= registeredBudgets[i].ToDate && 
                        bill.Category == registeredBudgets[i].Category && 
                        bill.DeletedBill == false)
                    .Sum(x => x.BaseValue);
            }

            return registeredBudgets;
        }

        public decimal GetWalletValue()
        {
            var inputs = repository.GetAll(user).Where(x => x.Wallet != (int)WalletBillStatus.InWallet).Sum(x => x.BaseValue);
            var outputs = repository.GetAll(user).Where(x => x.Wallet != (int)WalletBillStatus.OutWallet).Sum(x => x.BaseValue);

            return inputs - outputs;
        }
    }
}
